package com.dap.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.dap.cache.common.RedisComponentInterface;
import com.dap.commons.WechatUserInfo;
import com.dap.commons.WechatXCXUserPhoneInfo;
import com.dap.commons.WechatXCXWatermark;
import com.dap.commons.XCXLoginResult;
import com.dap.exception.BusinessException;
import com.dap.param.WechatMessageParam;
import com.dap.utils.password.AESUtil;
import com.dap.utils.password.SHA1Util;
import com.google.common.base.Charsets;
import com.google.common.collect.Sets;
import org.apache.commons.codec.binary.Hex;
import org.apache.http.NameValuePair;
import org.apache.http.entity.ContentType;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileOutputStream;
import java.io.Serializable;
import java.security.AlgorithmParameters;
import java.util.*;

public class WechatUtils {
    private final static Logger logger = LoggerFactory.getLogger(WechatUtils.class);
    public final static String WECHAT_ACCESS_TOKEN_KEY = "wechatAccessToken";
    public final static String WXCHAT_TICKET_KEY = "wechatTicket";

    private static ThreadLocal<MyHttpClient> httpClientThreadLocal = new ThreadLocal<>();
    private static MyHttpClient generateHttpClient() {
        MyHttpClient httpClient = httpClientThreadLocal.get();
        if (null != httpClient)
            return httpClient;

        httpClient = new MyHttpClient();
        httpClientThreadLocal.set(httpClient);

        return httpClient;
    }



    public static class WechatAppInfo {
        public String appName;
        public String appId;
        public String appSecret;
        public WechatAppInfo(String appName, String appId, String appSecret) {
            this.appName = appName;
            this.appId = appId;
            this.appSecret = appSecret;
        }
    }
    private final Map<String, WechatAppInfo> wechatAppInfoMapByAppName;
    private final Map<String, WechatAppInfo> wechatAppInfoMapByAppId;
    private final RedisComponentInterface redis;
    public WechatUtils(String appNamesStr, String appIdsStr, String appSecretsStr, RedisComponentInterface redis) throws Exception {
        wechatAppInfoMapByAppName = new HashMap<>();
        wechatAppInfoMapByAppId = new HashMap<>();

        if (!StringUtils.isEmpty(appNamesStr) && !StringUtils.isEmpty(appIdsStr) && !StringUtils.isEmpty(appSecretsStr)) {
            String[] appNameArray = appNamesStr.split(",");
            String[] appIdArray = appIdsStr.split(",");
            String[] appSecretArray = appSecretsStr.split(",");

            if (appNameArray.length != appIdArray.length || appIdArray.length != appSecretArray.length)
                throw new Exception("错误的微信 appId appSecret 配置, appIdsStr=" + appIdsStr + ", appSecretsStr=" + appSecretsStr);

            for (int i = 0; i < appIdArray.length; ++i) {
                WechatAppInfo wechatAppInfo = new WechatAppInfo(appNameArray[i], appIdArray[i], appSecretArray[i]);
                wechatAppInfoMapByAppName.put(appNameArray[i], wechatAppInfo);
                wechatAppInfoMapByAppId.put(appIdArray[i], wechatAppInfo);
            }
        }

        this.redis = redis;
    }



    public WechatAppInfo getWechatAppByAppId(String appId) {
        return wechatAppInfoMapByAppId.get(appId);
    }
    public String getWechatAppSecretByAppId(String appId) {
        return wechatAppInfoMapByAppId.get(appId).appSecret;
    }
    public WechatAppInfo getWechatAppByAppName(String appName) {
        return wechatAppInfoMapByAppName.get(appName);
    }



    public static class WechatUserOpenIdAccessToken {
        public String openid;
        public String access_token;
    }
    public WechatUserOpenIdAccessToken getWechatUserOpenIdAccessToken(String userCode, String appId) throws Exception {
        MyHttpClient httpClient = generateHttpClient();

        StringBuffer buffer = new StringBuffer("https://api.weixin.qq.com/sns/oauth2/access_token?grant_type=authorization_code");
        buffer.append("&code=").append(userCode);
        buffer.append("&appid=").append(appId);
        buffer.append("&secret=").append(getWechatAppSecretByAppId(appId));

        String json = httpClient.get(buffer.toString());
        return JSON.parseObject(json, WechatUserOpenIdAccessToken.class);
    }


    private static class WechatAppAccessToken {
        public String access_token;
        public Integer expires_in;
    }
    // 获取app的全局accessToken
    private WechatAppAccessToken getWechatAppAccessToken(String appId) throws Exception {
        MyHttpClient httpClient = generateHttpClient();

        StringBuffer buffer = new StringBuffer("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential");
        buffer.append("&appid=").append(appId);
        buffer.append("&secret=").append(getWechatAppSecretByAppId(appId));

        String json = httpClient.get(buffer.toString());
        return JSON.parseObject(json, WechatAppAccessToken.class);
    }
    public String getWechatAppAccessTokenWithCache(String appId, boolean isReadCache) {
        if (isReadCache) {
            // 读取缓存
            try {
                String accessToken = redis.get(WECHAT_ACCESS_TOKEN_KEY, appId, String.class);
                if (!StringUtils.isEmpty(accessToken))
                    return accessToken;
            } catch (Exception e) {
                logger.info("", e);
            }
        }


        WechatAppAccessToken wechatAppAccessToken;
        try {
            wechatAppAccessToken = getWechatAppAccessToken(appId);
            if (null == wechatAppAccessToken || StringUtils.isEmpty(wechatAppAccessToken.access_token))
                return null;
        } catch (Exception e) {
            logger.info("", e);
            return null;
        }


        // 设置缓存
        try {
            if (isReadCache)
                redis.setnxExpire(WECHAT_ACCESS_TOKEN_KEY, appId, wechatAppAccessToken.expires_in, wechatAppAccessToken.access_token);
            else
                redis.set(WECHAT_ACCESS_TOKEN_KEY, appId, wechatAppAccessToken.expires_in, wechatAppAccessToken.access_token);
        } catch (Exception e) {
            logger.info("", e);
        }

        return wechatAppAccessToken.access_token;
    }


    public static class WechatMessageRespone implements Serializable {
        public int errcode;
        public String errmsg;
        public String msgid;
    }
    public WechatMessageRespone sendWechatMessage(String appId, WechatMessageParam param) throws Exception {
        String appAccessToken = getWechatAppAccessTokenWithCache(appId, true);
        MyHttpClient httpClient = generateHttpClient();

        String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + appAccessToken;
        if (StringUtils.isEmpty(param.getTouser()) || StringUtils.isEmpty(param.getTemplate_id()) || StringUtils.isEmpty(param.getData())) {
            throw new BusinessException(com.dap.commons.Constants.ResponseCode.GENERAL_EXCEPTION, "必传参数不能为空！");
        }

        // 成功返回的格式为{"errcode":0, "errmsg":"ok", "msgid":200228332}
        byte[] response = httpClient.postBytesResult(url, JSONObject.toJSONString(param).getBytes(Charsets.UTF_8), ContentType.APPLICATION_JSON);
        return JSONObject.parseObject(response, WechatMessageRespone.class);
    }


    // 静默授权 方式 获取用户信息
    private String getWechatUserInfoAutoAuthInternal(String openId, String appAccessToken) throws Exception {
        MyHttpClient httpClient = generateHttpClient();

        StringBuffer buffer = new StringBuffer("https://api.weixin.qq.com/cgi-bin/user/info?lang=zh_CN&access_token=");
        buffer.append(appAccessToken).append("&openid=").append(openId);

        return httpClient.get(buffer.toString());
    }
    private String getWechatUserInfoAutoAuthInternal(String openId, String appId, boolean isReadAccessTokenCache) {
        // 获取 accessToken
        String appAccessToken = getWechatAppAccessTokenWithCache(appId, isReadAccessTokenCache);
        if (StringUtils.isEmpty(appAccessToken)) {
            throw new BusinessException(com.dap.commons.Constants.ResponseCode.GENERAL_EXCEPTION, "cannot get app accessToken");
        }

        // 获取 unionId
        String wechatUserInfoJsonStr;
        try {
            wechatUserInfoJsonStr = getWechatUserInfoAutoAuthInternal(openId, appAccessToken);
        } catch (Exception e) {
            logger.info("", e);
            throw new BusinessException(com.dap.commons.Constants.ResponseCode.GENERAL_EXCEPTION, "cannot get wechat user info");
        }
        return wechatUserInfoJsonStr;
    }
    private static final Set<String> retryAccessTokenCodeSet = Sets.newHashSet("40001", "42001", "42007", "40014");
    public WechatUserInfo getWechatUserInfoAutoAuth(String openId, String appId) {
        String wechatUserInfoJsonStr = getWechatUserInfoAutoAuthInternal(openId, appId, true);
        WechatUserInfo wechatUserInfo = JSON.parseObject(wechatUserInfoJsonStr, WechatUserInfo.class);
        if (retryAccessTokenCodeSet.contains(wechatUserInfo.errcode)) {
            wechatUserInfoJsonStr = getWechatUserInfoAutoAuthInternal(openId, appId, false);
            wechatUserInfo = JSON.parseObject(wechatUserInfoJsonStr, WechatUserInfo.class);
        }
        String unionId = wechatUserInfo.unionid;
        if (StringUtils.isEmpty(unionId)) {
            logger.info("unionId is invalid, msg={}", wechatUserInfoJsonStr);
            throw new BusinessException(com.dap.commons.Constants.ResponseCode.GENERAL_EXCEPTION, "unionId is invalid");
        }

        return wechatUserInfo;
    }
    public String getUnionIdAutoAuth(String openId, String appId) {
        return getWechatUserInfoAutoAuth(openId, appId).unionid;
    }





    // 手动授权 方式 获取用户信息
    private String getWechatUserInfoManualAuthInternal(String userAccessToken, String openId) throws Exception {
        StringBuffer buffer = new StringBuffer("https://api.weixin.qq.com/sns/userinfo?lang=zh_CN&access_token=");
        buffer.append(userAccessToken).append("&openid=").append(openId);

        return generateHttpClient().get(buffer.toString());
    }
    public WechatUserInfo getWechatUserInfoManualAuth(String userAccessToken, String openId) throws Exception {
        String wechatUserInfoJsonStr = getWechatUserInfoManualAuthInternal(userAccessToken, openId);

        WechatUserInfo wechatUserInfo = JSON.parseObject(wechatUserInfoJsonStr, WechatUserInfo.class);
        String unionId = wechatUserInfo.unionid;
        if (StringUtils.isEmpty(unionId)) {
            logger.info("unionId is invalid, msg={}", wechatUserInfoJsonStr);
            throw new BusinessException(com.dap.commons.Constants.ResponseCode.GENERAL_EXCEPTION, "unionId is invalid");
        }

        return wechatUserInfo;
    }
    public String getUnionIdManualAuth(String userAccessToken, String openId) throws Exception {
        return getWechatUserInfoManualAuth(userAccessToken, openId).unionid;
    }




    /**
     * 微信小程序登录
     */
    public XCXLoginResult xcxLogin(String appId, String code) {
        String secret = getWechatAppSecretByAppId(appId);

        StringBuilder buider = new StringBuilder();
        buider.append("https://api.weixin.qq.com/sns/jscode2session?appid=" + appId);
        buider.append("&secret=" + secret);
        buider.append("&js_code=" + code);
        buider.append("&grant_type=authorization_code");
        String resultStr = generateHttpClient().get(buider.toString());
        logger.info("xcx login, appId={}, code={}, result:{}", appId, code, resultStr);

        if (resultStr == null) {
            return null;
        }
        return JSON.parseObject(resultStr, XCXLoginResult.class);
    }




    private String getWechatTicketInternal(String appId, boolean isReadAccessTokenCache) throws Exception {
        // 获取accessToken
        String accessToken = getWechatAppAccessTokenWithCache(appId, isReadAccessTokenCache);
        if (StringUtils.isEmpty(accessToken)) {
            return null;
        }

        String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=" + accessToken;
        return generateHttpClient().get(url);
    }
    private static class WechatTicketInfo {
        public String errcode;
        public String ticket;
        public Integer expires_in;
    }
    public String getWechatTicket(String appId) {
        // 首先从缓存中找出数据
        try {
            String ticket = redis.get(WXCHAT_TICKET_KEY, appId, String.class);
            if (!StringUtils.isEmpty(ticket))
                return ticket;
        } catch (Exception e) {
            logger.info("", e);
        }

        try {
            String wechatTicketInfoStr = getWechatTicketInternal(appId, true);
            if (StringUtils.isEmpty(wechatTicketInfoStr))
                return null;

            // 解析返回结果
            WechatTicketInfo wechatTicketInfo = JSONObject.parseObject(wechatTicketInfoStr, WechatTicketInfo.class);
            if ("0".equals(wechatTicketInfo.errcode)) {
                try {
                    redis.setnxExpire(WXCHAT_TICKET_KEY, appId, wechatTicketInfo.expires_in, wechatTicketInfo.ticket);
                } catch (Exception e) {
                    logger.info("", e);
                }
                return wechatTicketInfo.ticket;
            } else {
                logger.error("appId[" + appId + "]获取微信ticket异常，响应数据为：" + wechatTicketInfoStr);
                return null;
            }
        } catch (Exception e) {
            logger.error("获取微信分享ticket异常", e);
            return null;
        }
    }


    /**
     * @Title 微信分享
     */
    public static class WechatShareVO implements Serializable {
        private static final long serialVersionUID = 2487884343342228719L;
        private String appId;
        private String nonceStr;
        private String timestamp;
        private String signature;

        public String getAppId() {
            return appId;
        }
        public void setAppId(String appId) {
            this.appId = appId;
        }
        public String getNonceStr() {
            return nonceStr;
        }
        public void setNonceStr(String nonceStr) {
            this.nonceStr = nonceStr;
        }
        public String getTimestamp() {
            return timestamp;
        }
        public void setTimestamp(String timestamp) {
            this.timestamp = timestamp;
        }
        public String getSignature() {
            return signature;
        }
        public void setSignature(String signature) {
            this.signature = signature;
        }
    }
    // 创建签名SHA1
    private static String createSHA1Sign(SortedMap<String, String> signParams) throws Exception {
        StringBuffer sb = new StringBuffer();
        Set es = signParams.entrySet();
        Iterator it = es.iterator();
        while (it.hasNext()) {
            Map.Entry entry = (Map.Entry) it.next();
            String k = (String) entry.getKey();
            String v = (String) entry.getValue();
            sb.append(k + "=" + v + "&");
            // 要采用URLENCODER的原始值！
        }
        String params = sb.substring(0, sb.lastIndexOf("&"));

        byte[] bytes = SHA1Util.digest(params.getBytes("GBK"));
        return Hex.encodeHexString(bytes);
    }
    public static WechatShareVO signWechatShare(String ticket, String url, String appId) throws Exception {
        String nonceStr = UUID.randomUUID().toString();
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        SortedMap<String, String> paramMap = new TreeMap<>();
        paramMap.put("jsapi_ticket", ticket);
        paramMap.put("noncestr", nonceStr);
        paramMap.put("timestamp", timestamp);
        paramMap.put("url", url);
        String signature = createSHA1Sign(paramMap);

        WechatShareVO resultVO = new WechatShareVO();
        resultVO.setAppId(appId);
        resultVO.setNonceStr(nonceStr);
        resultVO.setTimestamp(timestamp);
        resultVO.setSignature(signature);
        return resultVO;
    }


    /**
     * 生成小程序码
     *
     * @param accessToken
     * @param scene
     * @return
     */
    public static byte[] createXcxQRCodeWithScene(String accessToken, String page,
                                                  String scene, Integer width, Map<String, String> lineColor) {
        final String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;

        final List<NameValuePair> params = new ArrayList<>();
        params.add(new BasicNameValuePair("scene", scene));
        if (width != null) {
            params.add(new BasicNameValuePair("width", String.valueOf(width)));
        }
        params.add(new BasicNameValuePair("page", page));
        if (lineColor != null && !lineColor.isEmpty()) {
            params.add(new BasicNameValuePair("line_color", String.valueOf(lineColor)));
        }

        logger.info("qrcode create request data: " + JSON.toJSONString(params));

        return generateHttpClient().postBytesResult(url, params);
    }
    /**
     * 生成永久小程序码
     */
    public static byte[] createXcxQRCodeForever(String accessToken, String path,
                                                  Integer width) {
        final String url = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode?access_token=" + accessToken;

        final Map<String, Object> params = new HashMap<>();
        params.put("path", path);
        if (width != null) {
            params.put("width", width);
        }

        logger.info("qrcode create request data: " + JSON.toJSONString(params));

        return generateHttpClient().postBytesResult(url, JSON.toJSONBytes(params), ContentType.APPLICATION_JSON);
    }


    public static boolean validateSignature(String validateSignature, String rawData, String sessionKey) {
        byte[] bytes = (rawData + sessionKey).getBytes(Charsets.UTF_8);
        byte[] signatureBytes = SHA1Util.digest(bytes);
        String signature = Hex.encodeHexString(signatureBytes);
        return signature.equals(validateSignature);
    }

    private static AlgorithmParameters generateIV(byte[] ivBytes) throws Exception {
        AlgorithmParameters params = AlgorithmParameters.getInstance("AES");
        params.init(new IvParameterSpec(ivBytes));
        return params;
    }
    /**
     * 删除解密后明文的补位字符
     */
    private static byte[] removePadBytes(byte[] decrypted) {
        int pad = decrypted[decrypted.length - 1];
        if (pad < 1 || pad > 32) {
            pad = 0;
        }
        return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad);
    }
    private static String aesDecrypt(byte[] srcBytes, byte[] keyBytes, byte[] ivBytes) throws Exception {
        try {
            // 判断Key是否正确
            if (keyBytes == null) {
                logger.info("Key为空null");
                return null;
            }
            // 判断Key是否为16位
            if (keyBytes.length != 16) {
                logger.info("Key长度不是16位");
                return null;
            }

            SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");
            AlgorithmParameters algorithmParameters = generateIV(ivBytes);

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, algorithmParameters);

            try {
                byte[] original = cipher.doFinal(srcBytes);
                original = removePadBytes(original);
                return new String(original, Charsets.UTF_8);
            } catch (Exception e) {
                logger.warn("", e);
                return null;
            }
        } catch (Exception ex) {
            logger.warn("", ex);
            return null;
        }
    }
    public static JSONObject resolveInfoFromXCXEncryptedData(String encryptedData, String iv, String sessionKey, String wechatAppId) throws Exception {
        // 加密秘钥
        byte[] keyByte = Base64Utils.decode(sessionKey.getBytes(Charsets.UTF_8));
        // 被加密的数据
        byte[] dataByte = Base64Utils.decode(encryptedData.getBytes(Charsets.UTF_8));
        // 偏移量
        byte[] ivByte = Base64Utils.decode(iv.getBytes(Charsets.UTF_8));

        try {
            // 如果密钥不足16位，那么就补足.  这个if 中的内容很重要
            int base = 16;
            if (keyByte.length % base != 0) {
                int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
                byte[] temp = new byte[groups * base];
                Arrays.fill(temp, (byte) 0);
                System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
                keyByte = temp;
            }

            String decrypted = aesDecrypt(dataByte, keyByte, ivByte);
            if (null == decrypted) {
                return null;
            }

            JSONObject jsonObject = JSON.parseObject(decrypted);
            WechatXCXWatermark watermark = jsonObject.getObject("watermark", WechatXCXWatermark.class);
            if (null == watermark || !wechatAppId.equals(watermark.appid)) {
                return null;
            }
            return jsonObject;
        } catch (Exception e) {
            logger.info("", e);
        }

        return null;
    }
    public static String resolveMobileFromXCXEncryptedData(String encryptedData, String iv, String sessionKey, String wechatAppId) throws Exception {
        JSONObject jsonObject = resolveInfoFromXCXEncryptedData(encryptedData, iv, sessionKey, wechatAppId);
        if (null == jsonObject) {
            return null;
        } else {
            WechatXCXUserPhoneInfo wechatXCXUserPhoneInfo = jsonObject.toJavaObject(WechatXCXUserPhoneInfo.class);
            return wechatXCXUserPhoneInfo.purePhoneNumber;
        }
    }



    public static void main(String[] args) throws Exception {
        WechatUtils wechatUtils = new WechatUtils("xcx", "wxb9a4a27e582dd36d", "608f75caf96ba40c2885b3f8f8292eae", null);

        WechatAppAccessToken wechatAppAccessToken = wechatUtils.getWechatAppAccessToken("wxb9a4a27e582dd36d");
        logger.info("accessToken={}", wechatAppAccessToken.access_token);

        byte[] bytes = createXcxQRCodeForever(wechatAppAccessToken.access_token, "pages/groupIntroduce/groupIntroduce", 180);

        FileOutputStream fos = new FileOutputStream(new File("C:\\Users\\aaa\\Desktop\\t.png"));
        fos.write(bytes);
        fos.close();
    }
}
